﻿using System;
using System.Collections.Generic;

namespace TLang.Ast
{
    using Values;

    public class Call : Node
    {
        public Node op;
        public Argument args;


        public Call(Node op, Argument args, String file, int start, int end, int line, int col) : base(file, start, end, line, col)
        {
            this.op = op;
            this.args = args;
        }


        public override Value Interp(Scope s)
        {
            Value opv = this.op.Interp(s);
            if (opv is Closure)
            {
                Closure closure = (Closure)opv;
                Scope funScope = new Scope(closure.env);
                List<Name> prams = closure.fun.prams;

                // set default values for parameters
                if (closure.properties != null)
                {
                    Declare.mergeDefault(closure.properties, funScope);
                }

                if (!args.positional.IsEmpty() && args.keywords.IsEmpty())
                {
                    for (int i = 0; i < args.positional.Count; i++)
                    {
                        Value value = args.positional[i].Interp(s);
                        funScope.PutValue(prams[i].Id, value);
                    }
                }
                else
                {
                    // try to bind all arguments
                    foreach (Name param in prams)
                    {
                        Node actual = args.keywords.Get(param.Id);
                        if (actual != null)
                        {
                            Value value = actual.Interp(funScope);
                            funScope.PutValue(param.Id, value);
                        }
                    }
                }
                return closure.fun.body.Interp(funScope);
            }
            else if (opv is RecordType)
            {
                RecordType template = (RecordType)opv;
                Scope values = new Scope();

                // set default values for fields
                Declare.mergeDefault(template.properties, values);

                // instantiate
                return new RecordValue(template.name, template, values);
            }
            else if (opv is PrimFun)
            {
                PrimFun prim = (PrimFun)opv;
                List<Value> args = Node.InterpList(this.args.positional, s);
                return prim.Apply(args, this);
            }
            else
            {  // can't happen
                Util.Abort(this.op, "calling non-function: " + opv);
                return Value.VOID;
            }
        }

        public override Value Typecheck(Scope s)
        {
            Value fun = this.op.Typecheck(s);
            if (fun is FunType)
            {
                FunType funtype = (FunType)fun;
                //            TypeChecker.self.uncalled.remove(funtype);

                Scope funScope = new Scope(funtype.env);
                List<Name> prams = funtype.fun.prams;

                // set default values for parameters
                if (funtype.properties != null)
                {
                    Declare.mergeType(funtype.properties, funScope);
                }

                if (!args.positional.IsEmpty() && args.keywords.IsEmpty())
                {
                    // positional
                    if (args.positional.Count != prams.Count)
                    {
                        Util.Abort(this.op,
                                "calling function with wrong number of arguments. expected: " + prams.Count
                                        + " actual: " + args.positional.Count);
                    }

                    for (int i = 0; i < args.positional.Count; i++)
                    {
                        Value value = args.positional[i].Typecheck(s);
                        Value expected = funScope.Lookup(prams[i].Id);
                        if (!Type.subtype(value, expected, false))
                        {
                            Util.Abort(args.positional[i], "type error. expected: " + expected + ", actual: " + value);
                        }
                        funScope.PutValue(prams[i].Id, value);
                    }
                }
                else
                {
                    // keywords
                    ISet<String> seen = new HashSet<String>();

                    // try to bind all arguments
                    foreach (Name param in prams)
                    {
                        Node actual = args.keywords.Get(param.Id);
                        if (actual != null)
                        {
                            seen.Add(param.Id);
                            Value value = actual.Typecheck(funScope);
                            Value expected = funScope.Lookup(param.Id);
                            if (!Type.subtype(value, expected, false))
                            {
                                Util.Abort(actual, "type error. expected: " + expected + ", actual: " + value);
                            }
                            funScope.PutValue(param.Id, value);
                        }
                        else
                        {
                            Util.Abort(this, "argument not supplied for: " + param);
                            return Value.VOID;
                        }
                    }

                    // detect extra arguments
                    List<String> extra = new List<string>();
                    foreach (String id in args.keywords.Keys)
                    {
                        if (!seen.Contains(id))
                        {
                            extra.Add(id);
                        }
                    }

                    if (!extra.IsEmpty())
                    {
                        Util.Abort(this, "extra keyword arguments: " + extra);
                        return Value.VOID;
                    }
                }

                Object retType = funtype.properties.LookupPropertyLocal(Constants.RETURN_ARROW, "type");
                if (retType != null)
                {
                    if (retType is Node)
                    {
                        // evaluate the return type because it might be (typeof x)
                        return ((Node)retType).Typecheck(funScope);
                    }
                    else
                    {
                        Util.Abort("illegal return type: " + retType);
                        return null;
                    }
                }
                else
                {
                    if (TypeChecker.self.callStack.Contains((FunType)fun))
                    {
                        Util.Abort(op, "You must specify return type for recursive functions: " + op);
                        return null;
                    }

                    TypeChecker.self.callStack.Add((FunType)fun);
                    Value actual = funtype.fun.body.Typecheck(funScope);
                    TypeChecker.self.callStack.Remove((FunType)fun);
                    return actual;
                }
            }
            else if (fun is RecordType)
            {
                RecordType template = (RecordType)fun;
                Scope values = new Scope();

                // set default values for fields
                Declare.mergeDefault(template.properties, values);

                // set actual values, overwrite defaults if any
                foreach (var e in args.keywords)
                {
                    if (!template.properties.Keys.Contains(e.Key))
                    {
                        Util.Abort(this, "extra keyword argument: " + e.Key);
                    }

                    Value actual = args.keywords.Get(e.Key).Typecheck(s);
                    Value expected = template.properties.LookupLocalType(e.Key);
                    if (!Type.subtype(actual, expected, false))
                    {
                        Util.Abort(this, "type error. expected: " + expected + ", actual: " + actual);
                    }
                    values.PutValue(e.Key, e.Value.Typecheck(s));
                }

                // check uninitialized fields
                foreach (String field in template.properties.Keys)
                {
                    if (values.LookupLocal(field) == null)
                    {
                        Util.Abort(this, "field is not initialized: " + field);
                    }
                }

                // instantiate
                return new RecordValue(template.name, template, values);
            }
            else if (fun is PrimFun)
            {
                PrimFun prim = (PrimFun)fun;
                if (prim.Arity >= 0 && args.positional.Count != prim.Arity)
                {
                    Util.Abort(this, "incorrect number of arguments for primitive " +
                            prim.Name + ", expecting " + prim.Arity + ", but got " + args.positional.Count);
                    return null;
                }
                else
                {
                    List<Value> args = Node.TypecheckList(this.args.positional, s);
                    return prim.Typecheck(args, this);
                }
            }
            else
            {
                Util.Abort(this.op, "calling non-function: " + fun);
                return Value.VOID;
            }

        }


        public override String ToString()
        {
            if (args.positional.Count != 0)
            {
                return "(" + op + " " + args + ")";
            }
            else
            {
                return "(" + op + ")";
            }
        }
    }
}
